﻿namespace UsefulTools.ExtensionMethods
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    using Core;


    public static class Collections
    {
        /// <summary>
        /// Selects items contained in a tree structure. 
        /// </summary>
        /// <typeparam name="TSource"></typeparam>
        /// <param name="source">A collection of top-level items in the tree.</param>
        /// <param name="selector">A function for traversing the tree. Should select inner tree level.</param>
        /// <param name="predicate">Its result whether the element should be selected and its children traversed.</param>
        /// <param name="leafsOnly">Indicates whether only leaves should be selected</param>
        /// <returns>A collection of selected items in the tree</returns>
        public static IEnumerable<TSource> SelectManyRecursive<TSource>(this IEnumerable<TSource> source,
            Func<TSource, IEnumerable<TSource>> selector, Func<TSource, bool> predicate = null, bool leafsOnly = false)
        {
            Throw.IfNull(source, "source");

            if (predicate == null)
            {
                predicate = x => true;
            }

            if (leafsOnly)
            {
                return source.Where(predicate).SelectMany(Functional.Y<TSource, IEnumerable<TSource>>
                    (f => obj =>
                    {
                        IEnumerable<TSource> children = selector(obj) ?? new List<TSource>();
                        IEnumerable<TSource> selected = children.Where(predicate).SelectMany(f);
                        return selected.Any() ? selected : selected.Prepend(obj);
                    }));
            }
            else
            {
                return source.Where(predicate).SelectMany(Functional.Y<TSource, IEnumerable<TSource>>
                (f => obj => selector(obj).Where(predicate).SelectMany(f).Prepend(obj)));
            }
        }
        /// <summary>
        /// Concats the same collection number of times.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="items"></param>
        /// <param name="times"></param>
        /// <returns></returns>
        public static IEnumerable<T> Repeat<T>(this IEnumerable<T> items, int times)
        {
            Throw.IfNull(items);
            IEnumerable<T> result = items;
            for (int i = 0; i < times; i++)
            {
                result = result.Concat(items);
            }
            return result;
        }

        /// <summary>
        /// Returns a collection of strings resulted from invoking ToString on each element.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="elems"></param>
        /// <returns></returns>
        public static IEnumerable<string> AsStrings<T>(this IEnumerable<T> elems)
        {
            Throw.IfNull(elems);
            return elems.Select(e => e.ToString());
        }

        /// <summary>
        /// Joins the elements in pairs. Count of arguments must be even.
        /// </summary>
        /// <typeparam name="T1"></typeparam>
        /// <typeparam name="T2"></typeparam>
        /// <param name="elements"></param>
        /// <returns></returns>
        public static IList<Tuple<T1, T2>> Pairs<T1, T2>(params object[] elements)
        {
            Throw.IfNull(elements);
            Throw.If(elements.Any(x => x == null));
            Throw.If(elements.Length % 2 != 0);
            var result = new List<Tuple<T1, T2>>();
            for (int i = 0; i < elements.Length; i += 2)
            {
                result.Add(new Tuple<T1, T2>((T1)elements[i], (T2)elements[i + 1]));
            }
            return result;
        }
       
        /// <summary>
        /// Creates a ReadOnlyCollection based on given enumerable.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="collection"></param>
        /// <returns></returns>
        public static ReadOnlyCollection<T> ToReadonly<T>(this IEnumerable<T> collection)
        {
            Throw.IfNull(collection);
            return new ReadOnlyCollection<T>(collection.ToList());
        }
        /// <summary>
        /// Maps values in a dictionary while preserving the key of each value.
        /// </summary>
        /// <typeparam name="T1"></typeparam>
        /// <typeparam name="T2"></typeparam>
        /// <typeparam name="T3"></typeparam>
        /// <param name="dict"></param>
        /// <param name="selector">A function to create a new value based on existing dictionary value and key.</param>
        /// <returns></returns>
        public static IDictionary<T1, T3> MapValues<T1, T2, T3>(this IDictionary<T1, T2> dict, Func<T1, T2, T3> selector)
        {
            Throw.IfNull(dict);
            return dict.Select(pair => new KeyValuePair<T1, T3>(pair.Key, selector(pair.Key, pair.Value)))
                .ToDictionary(pair => pair.Key, pair => pair.Value);
        }

        /// <summary>
        /// Adds a variable number of items to the collection.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="collection"></param>
        /// <param name="toAdd"></param>
        public static void AddRange<T>(this ICollection<T> collection, params T[] toAdd)
        {
            Throw.IfNull(collection);
            Throw.IfNull(toAdd);
            collection.AddRange(toAdd.AsEnumerable());
        }
        /// <summary>
        /// Computes the average value for a collection or returns zero if the collection is empty.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="collection"></param>
        /// <param name="func"></param>
        /// <returns></returns>
        public static double AverageOrZero<T>(this IEnumerable<T> collection, Func<T, int> func)
        {
            Throw.IfNull(collection);
            return collection.Any() ? collection.Average(func) : 0;
        }
        /// <summary>
        /// Computes the average value for a collection or returns zero if the collection is empty.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="collection"></param>
        /// <param name="func"></param>
        /// <returns></returns>
        public static double AverageOrZero<T>(this IEnumerable<T> collection, Func<T, long> func)
        {
            Throw.IfNull(collection);
            return collection.Any() ? collection.Average(func) : 0;
        }
        /// <summary>
        /// Filters the collection preserving the items for which the predicate returns false.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="collection"></param>
        /// <param name="predicate"></param>
        /// <returns></returns>
        public static IEnumerable<T> WhereNot<T>(this IEnumerable<T> collection, Func<T, bool> predicate)
        {
            Throw.IfNull(collection);
            return collection.Where(e => !predicate(e));
        }

        /// <summary>
        /// Flattens the collection in collection relationship - returns all items in one collection.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="collection"></param>
        /// <returns></returns>
        public static IEnumerable<T> Flatten<T>(this IEnumerable<IEnumerable<T>> collection)
        {
            Throw.IfNull(collection);
            return collection.SelectMany(e => e);
        }

        /// <summary>
        /// Adds new items to the collection.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="collection"></param>
        /// <param name="toAdd"></param>
        public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> toAdd)
        {
            Throw.IfNull(collection);
            Throw.IfNull(toAdd);
            foreach (T item in toAdd)
            {
                collection.Add(item);
            }

        }

        /// <summary>
        /// Creates a dictionary based on given key space so it will contain at most as many items as in the key space.
        /// The value will not be inserted into the dictionary if its computed key is not contained in the key space.
        /// </summary>
        /// <typeparam name="T">Type of the values.</typeparam>
        /// <typeparam name="TKey">Type of the keys.</typeparam>
        /// <param name="collection">Collection of items to be contaned in the dictionary.</param>
        /// <param name="keySpace">Set of all keys that are allowed in the dictionary.</param>
        /// <param name="keySelector">The function to compute dictionary key based on a value.</param>
        /// <returns></returns>
        public static Dictionary<TKey, T> ToSparseDictionary<T, TKey>(this IEnumerable<T> collection,
            IEnumerable<TKey> keySpace, Func<T, TKey> keySelector)
        {
            Throw.IfNull(collection, "collection");
            Throw.IfNull(keySpace, "keySpace");
            Throw.IfNull(keySelector, "keySelector");
            var resultDictionary = new Dictionary<TKey, T>();
            var keys = keySpace.Distinct().ToList();
            foreach (T item in collection)
            {
                var key = keySelector(item);
                if (keys.Contains(key))
                {
                    keys.Remove(key);
                    resultDictionary.Add(key, item);
                    if (keys.Count == 0)
                    {
                        break;
                    }
                }
            }
            return resultDictionary;
        }
      
        /// <summary>
        /// Returns the same collection or null if it is empty.
        /// </summary>
        /// <typeparam name="T">Type of items in collection.</typeparam>
        /// <param name="enumerable">The possibly-null collection.</param>
        /// <returns></returns>
        public static IEnumerable<T> ToEmptyIfNull<T>(this IEnumerable<T> enumerable)
        {
            return enumerable ?? Enumerable.Empty<T>();
        }

        /// <summary>
        /// Returns true if the collection contains any of the given values.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="enumerable"></param>
        /// <param name="val1"></param>
        /// <param name="val2"></param>
        /// <returns></returns>
        public static bool ContainsAny<T>(this IEnumerable<T> enumerable, params T[] values)
        {
            Throw.IfNull(enumerable);
            foreach (var item in enumerable)
            {
                if(values.Contains(item))
                {
                    return true;
                }
            }
            return false;
        }
        /// <summary>
        /// Returns true if the collection contains any of the given values.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="enumerable"></param>
        /// <param name="val1"></param>
        /// <param name="val2"></param>
        /// <returns></returns>
        public static bool ContainsAll<T>(this IEnumerable<T> enumerable, params T[] values)
        {
            Throw.IfNull(enumerable);
            foreach (var item in enumerable)
            {
                if (!values.Contains(item))
                {
                    return false;
                }
            }
            return true;
        }
        /// <summary>
        /// Creates a new NotifyingCollection based on given enumerable
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="collection"></param>
        /// <returns></returns>
        public static NotifyingCollection<T> ToNotifingCollection<T>(this IEnumerable<T> collection)
        {
            return new NotifyingCollection<T>(collection);
        }
       
    }
}